UpdateCommandBuilder.java
package org.codefilarete.stalactite.sql.order;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.codefilarete.stalactite.query.builder.PreparedSQLAppender;
import org.codefilarete.stalactite.query.builder.SQLAppender;
import org.codefilarete.stalactite.query.builder.SQLBuilder;
import org.codefilarete.stalactite.query.builder.StringSQLAppender;
import org.codefilarete.stalactite.query.builder.WhereSQLBuilderFactory.WhereSQLBuilder;
import org.codefilarete.stalactite.query.model.ColumnCriterion;
import org.codefilarete.stalactite.query.model.Placeholder;
import org.codefilarete.stalactite.query.model.Selectable;
import org.codefilarete.stalactite.query.model.UnitaryOperator;
import org.codefilarete.stalactite.query.model.ValuedVariable;
import org.codefilarete.stalactite.sql.Dialect;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.stalactite.sql.ddl.structure.Table;
import org.codefilarete.stalactite.sql.statement.DMLGenerator;
import org.codefilarete.stalactite.sql.statement.PreparedSQL;
import org.codefilarete.stalactite.sql.statement.binder.ParameterBinder;
import org.codefilarete.tool.collection.Arrays;
import org.codefilarete.tool.collection.Iterables;
import org.codefilarete.tool.trace.MutableInt;
/**
* A SQL builder for {@link Update} objects.
* Can hardly be mutualized with {@link DMLGenerator} because the latter doesn't handle multi
* tables update.
*
* @author Guillaume Mary
*/
public class UpdateCommandBuilder<T extends Table<T>> implements SQLBuilder {
private final Update<T> update;
private final Dialect dialect;
private final MultiTableAwareDMLNameProvider dmlNameProvider;
public UpdateCommandBuilder(Update<T> update, Dialect dialect) {
this.update = update;
this.dialect = dialect;
this.dmlNameProvider = new MultiTableAwareDMLNameProvider(dialect.getDmlNameProviderFactory());
}
@Override
public String toSQL() {
StringSQLAppender sqlAppender = new StringSQLAppender(dmlNameProvider);
appendUpdateStatement(sqlAppender, dmlNameProvider);
return sqlAppender.getSQL();
}
private void appendUpdateStatement(SQLAppender result, MultiTableAwareDMLNameProvider dmlNameProvider) {
appendUpdateStatement(result, result, dmlNameProvider);
}
private void appendUpdateStatement(SQLAppender setClauseAppender, SQLAppender criteriaAppender, MultiTableAwareDMLNameProvider dmlNameProvider) {
setClauseAppender.cat("update ");
// looking for additional Tables : more than the updated one, can be found in conditions
Set<Column<Table<?>, Object>> whereColumns = new LinkedHashSet<>();
update.getCriteria().forEach(c -> {
if (c instanceof ColumnCriterion && ((ColumnCriterion) c).getColumn() instanceof Column) {
whereColumns.add((Column<Table<?>, Object>) ((ColumnCriterion) c).getColumn());
Object condition = ((ColumnCriterion) c).getCondition();
if (condition instanceof UnitaryOperator
&& ((UnitaryOperator) condition).getValue() instanceof ValuedVariable
&& ((ValuedVariable) ((UnitaryOperator) condition).getValue()).getValue() instanceof Column) {
whereColumns.add((Column) ((ValuedVariable) ((UnitaryOperator) condition).getValue()).getValue());
}
}
});
Collection<? extends Table<?>> tablesInCondition = Iterables.collect(whereColumns, Column::getTable, HashSet::new);
tablesInCondition.remove(this.update.getTargetTable());
Collection<? extends Table<?>> additionalTables = tablesInCondition;
// update of the single-table-marker
dmlNameProvider.setMultiTable(!additionalTables.isEmpty());
setClauseAppender.cat(dmlNameProvider.getName(this.update.getTargetTable()))
.catIf(dmlNameProvider.isMultiTable(), ", ");
// additional tables (with optional alias)
Iterator<? extends Table<?>> iterator = additionalTables.iterator();
while (iterator.hasNext()) {
Table next = iterator.next();
setClauseAppender.cat(dmlNameProvider.getName(next)).catIf(iterator.hasNext(), ", ");
}
// append updated columns part
setClauseAppender.cat(" set ");
Iterator<ColumnVariable> columnIterator = update.getRow().stream()
.filter(ColumnVariable.class::isInstance)
.map(ColumnVariable.class::cast)
.iterator();
while (columnIterator.hasNext()) {
ColumnVariable<?, T> c = columnIterator.next();
setClauseAppender.cat(dmlNameProvider.getName(c.getColumn()), " = ");
appendToSetClause(c, setClauseAppender, dmlNameProvider);
setClauseAppender.catIf(columnIterator.hasNext(), ", ");
}
// append where clause
if (!update.getCriteria().getConditions().isEmpty()) {
criteriaAppender.cat(" where ");
WhereSQLBuilder whereSqlBuilder = dialect.getQuerySQLBuilderFactory().getWhereBuilderFactory().whereBuilder(this.update.getCriteria(), dmlNameProvider);
whereSqlBuilder.appendTo(criteriaAppender);
}
}
/**
* Method that must append the given value coming from the "set" clause to the given SQLAppender.
* Left protected to cover unexpected cases (because {@link InsertColumn} handle Objects as value)
*
* @param updateColumn the value to append in the {@link SQLAppender}
* @param result the final SQL appender
* @param dmlNameProvider provider of tables and columns names
*/
protected void appendToSetClause(ColumnVariable<?, T> updateColumn, SQLAppender result, MultiTableAwareDMLNameProvider dmlNameProvider) {
Object value = updateColumn.getValue();
if (value instanceof Column) {
// case Update.set(colA, colB)
result.cat(dmlNameProvider.getName((Column) value));
} else {
// case Update.set(colA, any object)
result.catValue(updateColumn.getColumn(), value);
}
}
public UpdateStatement<T> toStatement() {
// We ask for SQL generation through a PreparedSQLWrapper because we need SQL placeholders for where + update clause
MutableInt variableCounter = new MutableInt();
Map<Column<T, ?>, Integer> columnIndexes = new HashMap<>();
Map<String, Set<Integer>> placeholderIndexes = new HashMap<>();
PreparedSQLAppender setValuesAppender = new PreparedSQLAppender(new StringSQLAppender(dmlNameProvider), dialect.getColumnBinderRegistry()) {
@Override
public <V> PreparedSQLAppender catValue(@Nullable Selectable<V> column, Object value) {
PreparedSQLAppender result = super.catValue(column, value);
columnIndexes.put((Column) column, variableCounter.increment());
return result;
}
};
PreparedSQLAppender criteriaAppender = new PreparedSQLAppender(new StringSQLAppender(dmlNameProvider), dialect.getColumnBinderRegistry()) {
@Override
public <V> PreparedSQLAppender catValue(@Nullable Selectable<V> column, Object value) {
PreparedSQLAppender result = super.catValue(column, value);
if (value instanceof Placeholder) {
placeholderIndexes.computeIfAbsent(((Placeholder) value).getName(), name -> new HashSet<>())
.add(variableCounter.increment());
}
return result;
}
};
appendUpdateStatement(setValuesAppender, criteriaAppender, dmlNameProvider);
// final assembly
Map<Integer, Object> values = new HashMap<>(setValuesAppender.getValues());
criteriaAppender.getValues().forEach((key, value) -> values.put(key + setValuesAppender.getValues().size(), value));
Map<Integer, ParameterBinder<?>> parameterBinders = setValuesAppender.getParameterBinders();
criteriaAppender.getParameterBinders().forEach((key, value) -> parameterBinders.put(key + setValuesAppender.getValues().size(), value));
Iterator<PlaceholderVariable> placeholderIterator = update.getRow().stream()
.filter(PlaceholderVariable.class::isInstance)
.map(PlaceholderVariable.class::cast)
.iterator();
while (placeholderIterator.hasNext()) {
PlaceholderVariable<?, T> c = placeholderIterator.next();
Set<Integer> indexes = placeholderIndexes.get(c.getName());
if (indexes == null) {
throw new IllegalArgumentException("No placeholder named \"" + c.getName() + "\" found in statement, available are "
+ placeholderIndexes.keySet());
}
indexes.forEach(index -> values.put(index, c.getValue()));
}
UpdateStatement<T> result = new UpdateStatement<>(
setValuesAppender.getSQL() + criteriaAppender.getSQL(),
parameterBinders,
columnIndexes,
placeholderIndexes);
result.setValues(values);
return result;
}
/**
* A specialized version of {@link PreparedSQL} dedicated to {@link Update} so one can set column values of the update clause
* through {@link #setValue(Column, Object)} and make it {@link Update} "reusable".
* Here is a usage example:
* <pre>{@code
* UpdateStatement updateStatement = new UpdateCommandBuilder(this).toStatement(dialect.getColumnBinderRegistry());
* try (WriteOperation<Integer> writeOperation = dialect.getWriteOperationFactory().createInstance(updateStatement, connectionProvider)) {
* writeOperation.setValues(updateStatement.getValues());
* writeOperation.execute();
* }
* // eventually change some values and re-execute it
* updateStatement.setValue(..);
* }</pre>
*/
public static class UpdateStatement<T extends Table<T>> extends PreparedSQL implements WherableStatement {
private final Map<Column<T, Object>, Integer> columnIndexes;
private final Map<String, Set<Integer>> placeholderIndexes;
/**
* Single constructor, not expected to be used elsewhere than {@link UpdateCommandBuilder}.
*
* @param sql the update sql order as a prepared statement
* @param parameterBinders binder for prepared statement values
* @param columnIndexes indexes of the updated columns
* @param placeholderIndexes indexes of variables of criteria clause
*/
public UpdateStatement(String sql,
Map<Integer, ? extends ParameterBinder<?>> parameterBinders,
Map<? extends Column<T, ?>, Integer> columnIndexes,
Map<String, Set<Integer>> placeholderIndexes) {
super(sql, parameterBinders);
this.columnIndexes = (Map<Column<T, Object>, Integer>) columnIndexes;
this.placeholderIndexes = placeholderIndexes;
}
@Override
public void assertValuesAreApplyable() {
super.assertValuesAreApplyable();
Set<Placeholder> presentPlaceholders = getValues().values().stream()
.filter(Placeholder.class::isInstance)
.map(Placeholder.class::cast)
.collect(Collectors.toSet());
if (!presentPlaceholders.isEmpty()) {
throw new IllegalStateException("Statement expect values for placeholders: " + presentPlaceholders.stream()
.map(Placeholder::getName)
.collect(Collectors.joining(", ")));
}
}
/**
* Dedicated method to set values of updated {@link Column}s.
*
* @param column {@link Column} to be set
* @param value value applied on Column
*/
public <O> void setValue(Column<T, O> column, O value) {
Integer index = columnIndexes.get(column);
if (index == null) {
throw new IllegalArgumentException("Column " + column.getAbsoluteName() + " is not declared updatable with fixed value in the update clause");
}
setValue(index, value);
}
public <O> void setValue(String placeholderName, O value) {
Set<Integer> placeholderIndex = placeholderIndexes.get(placeholderName);
if (placeholderIndex == null) {
throw new IllegalArgumentException("Placeholder '" + placeholderName + "' is not declared as a criteria in the where clause");
}
placeholderIndex.forEach(index -> setValue(index, value));
}
}
}